/*
 * Copyright 2010-2012 VMware and contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springsource.loaded.test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.fail;

import java.lang.ref.Reference;
import java.lang.reflect.InvocationTargetException;
import java.util.List;

import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.springsource.loaded.GlobalConfiguration;
import org.springsource.loaded.NameRegistry;
import org.springsource.loaded.ReloadableType;
import org.springsource.loaded.TypeRegistry;
import org.springsource.loaded.test.infra.Result;
import org.springsource.loaded.test.infra.SubLoader;
import org.springsource.loaded.test.infra.SuperLoader;


/**
 * These tests are going to load things across classloaders and the reloadable behaviour should work. Typical scenarios:
 * <ul>
 * <li>A supertype and its subtype are loaded by different loaders
 * <li>A type and the type it is calling are loaded by different loaders
 * </ul>
 * 
 * @author Andy Clement
 * @since 1.0
 */
public class CrossLoaderTests extends SpringLoadedTests {

	private SubLoader subLoader;

	@After
	public void teardown() throws Exception {
		super.teardown();
		GlobalConfiguration.directlyDefineTypes = true;
	}

	@Before
	public void setup() throws Exception {
		super.setup();
		binLoader = new SubLoader();
		subLoader = (SubLoader) binLoader;
		GlobalConfiguration.directlyDefineTypes = false;
	}

	/**
	 * Check the basics - does the SubLoader/SuperLoader mechanism work.
	 */
	@Test
	public void loadTypesAcrossLoaders() throws Exception {
		ReloadableType rtypeA = subLoader.loadAsReloadableType("superpkg.Top");
		result = runUnguarded(rtypeA.getClazz(), "m");
		assertEquals("Top.m() running", result.stdout);

		ReloadableType rtypeB = subLoader.loadAsReloadableType("subpkg.Bottom");
		result = runUnguarded(rtypeB.getClazz(), "m");
		assertEquals("Bottom.m() running", result.stdout);
		assertNotSame(rtypeA.getTypeRegistry(), rtypeB.getTypeRegistry());
	}

	@Test
	public void reloadSubtype() throws Exception {
		subLoader.loadAsReloadableType("superpkg.Top");
		ReloadableType rtypeB = subLoader.loadAsReloadableType("subpkg.Bottom");

		result = runUnguarded(rtypeB.getClazz(), "m");
		assertEquals("Bottom.m() running", result.stdout);

		rtypeB.loadNewVersion("2", retrieveRename("subpkg.Bottom", "subpkg.Bottom002", "superpkg.Top002:superpkg.Top"));
		result = runUnguarded(rtypeB.getClazz(), "m");
		assertEquals("Bottom002.m() running", result.stdout);
	}

	@Test
	public void reloadSupertype() throws Exception {
		ReloadableType rtypeA = subLoader.loadAsReloadableType("superpkg.Top");
		subLoader.loadAsReloadableType("subpkg.Bottom");

		result = runUnguarded(rtypeA.getClazz(), "m");
		assertEquals("Top.m() running", result.stdout);

		rtypeA.loadNewVersion("2", retrieveRename("superpkg.Top", "superpkg.Top002"));
		result = runUnguarded(rtypeA.getClazz(), "m");
		assertEquals("Top002.m() running", result.stdout);
	}

	/**
	 * hierarchy loaded across classloaders. <br>
	 * Top - all versions have a method 'm()'. v003 has method 'newMethodOnTop()'<br>
	 * Bottom - all versions have a method 'm()'. v003 version of m() calls 'super.newMethodOnTop()'
	 */
	@Ignore
	// test currently failing because we cache the reloadable type descriptors in TypeRegistry.getDescriptorFor()
	@Test
	public void reloadSupertypeCalledThroughSubtype() throws Exception {
		String top = "superpkg.Top";
		String bot = "subpkg.Bottom";

		ReloadableType rtypeA = subLoader.loadAsReloadableType(top);
		ReloadableType rtypeB = subLoader.loadAsReloadableType(bot);

		rtypeA.loadNewVersion("2", retrieveRename(top, top + "003"));
		rtypeB.loadNewVersion("2", retrieveRename(bot, bot + "003", top + "003:" + top));

		// Check the registry looks right for Top
		int topId = NameRegistry.getIdFor("superpkg/Top");
		TypeRegistry trTop = TypeRegistry.getTypeRegistryFor(subLoader.getParent());
		assertEquals(0, topId);
		assertEquals(top, trTop.getReloadableType(topId).getName());
		assertEquals(top, trTop.getReloadableType("superpkg/Top").getName());

		int bottomId = NameRegistry.getIdFor("subpkg/Bottom");
		TypeRegistry trBot = TypeRegistry.getTypeRegistryFor(subLoader);
		assertEquals(1, bottomId);
		assertEquals(bot, trBot.getReloadableType(bottomId).getName());
		assertEquals(bot, trBot.getReloadableType("subpkg/Bottom").getName());

		// Now call the m() in the Bottom003 type, which calls super.newMethodOnTop()
		result = runUnguarded(rtypeB.getClazz(), "m");
		assertEquals("newMethodOnTop() running", result.stdout);
	}

	/**
	 * In a class loaded by the subloader, calling a new method in a class loaded by the superloader. (ivicheck)
	 */
	@Test
	public void reloadTargetInSuperloader() throws Exception {
		String target = "superpkg.Target";
		String invoker = "subpkg.Invoker";

		ReloadableType targetR = subLoader.loadAsReloadableType(target);
		ReloadableType invokerR = subLoader.loadAsReloadableType(invoker);

		targetR.loadNewVersion("2", retrieveRename(target, target + "002"));
		invokerR.loadNewVersion("2", retrieveRename(invoker, invoker + "002", target + "002:" + target));

		// Check the registry looks right for target
		int targetId = NameRegistry.getIdFor(toSlash(target));
		assertEquals(0, targetId);
		TypeRegistry trtarget = TypeRegistry.getTypeRegistryFor(subLoader.getParent());
		assertEquals(target, trtarget.getReloadableType(targetId).getName());
		assertEquals(target, trtarget.getReloadableType(toSlash(target)).getName());

		int invokerId = NameRegistry.getIdFor(toSlash(invoker));
		TypeRegistry trinvokerR = TypeRegistry.getTypeRegistryFor(subLoader);
		assertEquals(1, invokerId);
		assertEquals(invoker, trinvokerR.getReloadableType(invokerId).getName());
		assertEquals(invoker, trinvokerR.getReloadableType(toSlash(invoker)).getName());

		// Now call the run() in the Invoker type, which calls 'Target.m()' where Target is in a different loader
		// and has been reloaded
		result = runUnguarded(invokerR.getClazz(), "run");
		assertEquals("Target002.m() running", result.stdout);
	}

	/**
	 * In a class loaded by the subloader, calling a new STATIC method in a class loaded by the superloader.
	 */
	@Test
	public void reloadTargetInSuperloaderCallingStaticMethod() throws Exception {
		String target = "superpkg.TargetB";
		String invoker = "subpkg.InvokerB";

		ReloadableType targetR = subLoader.loadAsReloadableType(target);
		ReloadableType invokerR = subLoader.loadAsReloadableType(invoker);

		targetR.loadNewVersion("2", retrieveRename(target, target + "002"));
		invokerR.loadNewVersion("2", retrieveRename(invoker, invoker + "002", target + "002:" + target));

		// Check the registry looks right for target
		int targetId = NameRegistry.getIdFor(toSlash(target));
		assertEquals(0, targetId);
		TypeRegistry trtarget = TypeRegistry.getTypeRegistryFor(subLoader.getParent());
		assertEquals(target, trtarget.getReloadableType(targetId).getName());
		assertEquals(target, trtarget.getReloadableType(toSlash(target)).getName());

		int invokerId = NameRegistry.getIdFor(toSlash(invoker));
		TypeRegistry trinvokerR = TypeRegistry.getTypeRegistryFor(subLoader);
		assertEquals(1, invokerId);
		assertEquals(invoker, trinvokerR.getReloadableType(invokerId).getName());
		assertEquals(invoker, trinvokerR.getReloadableType(toSlash(invoker)).getName());

		// Now call the run() in the Invoker type, which calls 'Target.m()' where Target is in a different loader
		// and has been reloaded
		result = runUnguarded(invokerR.getClazz(), "run");
		assertEquals("TargetB002.m() running", result.stdout);
	}

	@Test
	public void superdispatchers() throws Exception {
		String sub = "subpkg.Controller";

		ReloadableType subR = subLoader.loadAsReloadableType(sub);

		Result result = runOnInstance(subR.getClazz(), subR.getClazz().newInstance(), "foo");
		assertEquals("grails.Top.foo() running\nsubpkg.ControllerB.foo() running", result.stdout);

		// Reload the subtype
		subR.loadNewVersion("2", retrieveRename(sub, sub + "002"));

		result = runOnInstance(subR.getClazz(), subR.getClazz().newInstance(), "foo");
		assertEquals("grails.Top.foo() running\nsubpkg.ControllerB.foo() running again!", result.stdout);
	}

	/**
	 * In a class loaded by the subloader, calling a new STATIC method in a class loaded by the superloader. (istcheck)
	 */
	@Test
	public void reloadTargetInSuperloaderCallingStaticMethod2() throws Exception {
		// start out same as previous test, then loads a further version:
		String target = "superpkg.TargetB";
		String invoker = "subpkg.InvokerB";

		ReloadableType targetR = subLoader.loadAsReloadableType(target);
		ReloadableType invokerR = subLoader.loadAsReloadableType(invoker);

		targetR.loadNewVersion("2", retrieveRename(target, target + "002"));
		invokerR.loadNewVersion("2", retrieveRename(invoker, invoker + "002", target + "002:" + target));

		// Now call the run() in the Invoker type, which calls 'Target.m()' where Target is in a different loader
		// and has been reloaded
		result = runUnguarded(invokerR.getClazz(), "run");
		assertEquals("TargetB002.m() running", result.stdout);

		// now new: load new version of target that is missing the method

		targetR.loadNewVersion("3", targetR.bytesInitial);
		try {
			result = runUnguarded(invokerR.getClazz(), "run");
			fail("");
		}
		catch (InvocationTargetException ite) {
			assertTrue(ite.getCause() instanceof NoSuchMethodError);
			assertEquals("TargetB.m()V", ite.getCause().getMessage());
		}
	}

	/**
	 * This is testing field access when the value of the field is being checked against what is allowed to be returned.
	 * With multiple classloaders around this can get a little messy, say the method returns 'Serializable' but the
	 * actual method returns a type that the reloadable types classloader can't see (something from a subloader).<br>
	 * Heres a trace when things go wrong:
	 * 
	 * Caused by: org.springsource.loaded.UnableToLoadClassException: Unable to find data for class 'subpkg/Subby' at
	 * org.springsource.loaded.Utils.loadClassAsBytes2(Utils.java:763) <br>
	 * at org.springsource.loaded.TypeRegistry.getDescriptorFor(TypeRegistry.java:246) <br>
	 * at org.springsource.loaded.Utils.isAssignableFrom(Utils.java:1480) <br>
	 * at org.springsource.loaded.Utils.checkCompatibility(Utils.java:1460) <br>
	 * at org.springsource.loaded.ISMgr.getValue(ISMgr.java:125) <br>
	 * at superpkg.TargetD.r$get(TargetD.java) <br>
	 * at superpkg.TargetD$$E2.getOne(TargetD002.java:17) <br>
	 * at superpkg.TargetD$$D2.getOne(Unknown Source) <br>
	 * at superpkg.TargetD.getOne(TargetD.java) <br>
	 * at subpkg.InvokerD.run(InvokerD.java:8)
	 */
	@Test
	public void reloadCheckingCompatibilityForReturnedFields() throws Exception {
		// start out same as previous test, then loads a further version:
		String target = "superpkg.TargetD";
		String invoker = "subpkg.InvokerD";

		ReloadableType targetR = subLoader.loadAsReloadableType(target);
		ReloadableType invokerR = subLoader.loadAsReloadableType(invoker);

		result = runUnguardedWithCCL(invokerR.getClazz(), subLoader, "run");
		assertEquals("null", result.stdout);

		targetR.loadNewVersion("2", retrieveRename(target, target + "002"));
		//		invokerR.loadNewVersion("2", retrieveRename(invoker, invoker + "002", target + "002:" + target));

		// Now call the run() in the Invoker type, which calls 'Target.m()' where Target is in a different loader
		// and has been reloaded
		result = runUnguardedWithCCL(invokerR.getClazz(), subLoader, "run");
		assertEquals("a subby", result.stdout);
	}

	/**
	 * In a class loaded by the subloader, calling a new method in a class loaded by the superloader using super<dot>.
	 * (ispcheck)
	 */
	@Test
	public void reloadTargetInSuperLoaderCallingSuper() throws Exception {
		String top = "superpkg.TopB";
		String bot = "subpkg.BottomB";

		ReloadableType rtypeA = subLoader.loadAsReloadableType(top);
		ReloadableType rtypeB = subLoader.loadAsReloadableType(bot);

		rtypeA.loadNewVersion("2", retrieveRename(top, top + "002"));
		rtypeB.loadNewVersion("2", retrieveRename(bot, bot + "002", top + "002:" + top));

		// Check the registry looks right for Top
		int topId = NameRegistry.getIdFor("superpkg/TopB");
		TypeRegistry trTop = TypeRegistry.getTypeRegistryFor(subLoader.getParent());
		assertEquals(0, topId);
		assertEquals(top, trTop.getReloadableType(topId).getName());
		assertEquals(top, trTop.getReloadableType("superpkg/TopB").getName());

		int bottomId = NameRegistry.getIdFor("subpkg/BottomB");
		TypeRegistry trBot = TypeRegistry.getTypeRegistryFor(subLoader);
		assertEquals(1, bottomId);
		assertEquals(bot, trBot.getReloadableType(bottomId).getName());
		assertEquals(bot, trBot.getReloadableType("subpkg/BottomB").getName());

		// Now call the m() in the Bottom003 type, which calls super.newMethodOnTop()
		result = runUnguarded(rtypeB.getClazz(), "m");
		assertEquals("TopB002.m() running", result.stdout);
	}

	/**
	 * Now calling through an interface loaded by the superloader (iincheck)
	 */
	@Test
	public void reloadTargetInterfaceIsInSuperloader() throws Exception {
		// start out same as previous test, then loads a further version:
		String target = "superpkg.TargetC";
		String targetImpl = "superpkg.TargetImplC";
		String invoker = "subpkg.InvokerC";

		ReloadableType targetR = subLoader.loadAsReloadableType(target);
		ReloadableType targetImplR = subLoader.loadAsReloadableType(targetImpl);
		ReloadableType invokerR = subLoader.loadAsReloadableType(invoker);

		targetR.loadNewVersion("2", retrieveRename(target, target + "002"));
		targetImplR.loadNewVersion("2", retrieveRename(targetImpl, targetImpl + "002", target + "002:" + target));
		invokerR.loadNewVersion("2",
				retrieveRename(invoker, invoker + "002", target + "002:" + target, targetImpl + "002:" + targetImpl));

		// Now call the run() in the Invoker type, which calls 'Target.m()' where Target is in a different loader
		// and has been reloaded
		result = runUnguarded(invokerR.getClazz(), "run");
		assertEquals("TargetImplC002.m() running", result.stdout);

		// now new: load new version of target that is missing the method

		targetR.loadNewVersion("3", targetR.bytesInitial);
		try {
			result = runUnguarded(invokerR.getClazz(), "run");
			fail("");
		}
		catch (InvocationTargetException ite) {
			assertTrue(ite.getCause() instanceof NoSuchMethodError);
			assertEquals("TargetC.m()V", ite.getCause().getMessage());
		}
	}

	/**
	 * Now calling through an interface loaded by the superloader (iincheck). This time a new method is added to the
	 * interface which is *already* implemented by the impl.
	 */
	@Test
	public void reloadTargetInterfaceIsInSuperloader2() throws Exception {
		// start out same as previous test, then loads a further version:
		String target = "superpkg.TargetC";
		String targetImpl = "superpkg.TargetImplC";
		String invoker = "subpkg.InvokerC";

		ReloadableType targetR = subLoader.loadAsReloadableType(target);
		ReloadableType targetImplR = subLoader.loadAsReloadableType(targetImpl);
		ReloadableType invokerR = subLoader.loadAsReloadableType(invoker);

		targetR.loadNewVersion("2", retrieveRename(target, target + "002"));
		targetImplR.loadNewVersion("2", retrieveRename(targetImpl, targetImpl + "002", target + "002:" + target));
		invokerR.loadNewVersion("2",
				retrieveRename(invoker, invoker + "003", target + "002:" + target, targetImpl + "002:" + targetImpl));

		// Now call the run() in the Invoker type, which calls 'Target.m()' where Target is in a different loader
		// and has been reloaded
		result = runUnguarded(invokerR.getClazz(), "run");
		assertEquals("TargetImplC002.n() running", result.stdout);

		// now new: load new version of target that is missing the method

		targetR.loadNewVersion("3", targetR.bytesInitial);
		try {
			result = runUnguarded(invokerR.getClazz(), "run");
			fail("");
		}
		catch (InvocationTargetException ite) {
			assertTrue(ite.getCause() instanceof NoSuchMethodError);
			assertEquals("TargetC.n()V", ite.getCause().getMessage());
		}
	}


	// Large scale test loading a bunch of types and verifying what happens in terms of tagging
	@Test
	public void verifyingAssociatedTypesInfo2() throws Exception {
		// associatedtypes (top and middle) are in the super loader
		// subassociatedtypes (bottom) are in the sub loader

		ReloadableType bm = subLoader.loadAsReloadableType("associatedtypes.CM");

		ReloadableType cm = subLoader.loadAsReloadableType("associatedtypes.CM");
		assertNotNull(cm);
		assertNotEquals(subLoader, cm.getClazz().getClassLoader());
		ReloadableType im1 = subLoader.loadAsReloadableType("associatedtypes.IM");
		assertNotNull(im1);

		runUnguarded(cm.getClazz(), "run"); // Cause clinit to run so associations are setup

		assertContains("associatedtypes.CM", toString(im1.getAssociatedSubtypes()));
		assertFalse(cm.isAffectedByReload());
		assertFalse(cm.isAffectedByReload());
		assertFalse(bm.isAffectedByReload());

		// Load CM again, should tag CM and IM
		cm.loadNewVersion("2", cm.bytesInitial);

		assertTrue(cm.isAffectedByReload());
		assertTrue(im1.isAffectedByReload());
		assertTrue(bm.isAffectedByReload());
	}

	public String toString(List<Reference<ReloadableType>> list) {
		if (list == null) {
			return "null";
		}
		StringBuilder b = new StringBuilder();
		b.append("[");
		for (int i = 0; i < list.size(); i++) {
			if (i > 0) {
				b.append(",");
			}
			Reference<ReloadableType> ref = list.get(i);
			ReloadableType rtype = ref.get();
			if (rtype != null) {
				b.append(rtype.getName());
			}
		}
		b.append("]");
		return b.toString();
	}

	// TODO unfinished do I have to worry about proxies loaded by sub classloaders or not?

	// Avoiding fastclass in this test, one less thing to worry about
	@Test
	public void cglibProxiesAcrossLoader1() throws Exception {

		binLoader = new SubLoader(new String[] {}, new String[] { "../testdata/lib/cglib-nodep-2.2.jar" });
		subLoader = (SubLoader) binLoader;

		String t = "subpkg.ProxyTestcase";

		ReloadableType proxyTestcaseR = subLoader.loadAsReloadableType(t);

		result = runUnguarded(proxyTestcaseR.getClazz(), "run");
		System.out.println(result);

		result = runUnguarded(proxyTestcaseR.getClazz(), "getProxyLoader");
		System.out.println(result.returnValue);

		result = runUnguarded(proxyTestcaseR.getClazz(), "getSimpleLoader");
		System.out.println(result.returnValue);

		//		Class<?> clazz = binLoader.loadClass(t);
		//
		//		runMethodAndCollectOutput(clazz, "configureTest1");
		//
		//		String output = runMethodAndCollectOutput(clazz, "run");
		//		// interception should have occurred and original should not have been run
		//		assertContains("[void example.Simple.moo()]", output);
		//		assertDoesNotContain("Simple.moo() running", output);
		//
		//		// Check we loaded it as reloadable
		//		ReloadableType rtype = TypeRegistry.getTypeRegistryFor(binLoader).getReloadableType(toSlash(t), false);
		//		assertNotNull(rtype);
		//
		//		// Check the incidental types were loaded as reloadable
		//		ReloadableType rtype2 = TypeRegistry.getTypeRegistryFor(binLoader).getReloadableType(toSlash("example.Simple"), false);
		//		assertNotNull(rtype2);
		//
		//		rtype.loadNewVersion(retrieveRename(t, t + "2", "example.Simple2:example.Simple"));
		//		rtype2.loadNewVersion(retrieveRename("example.Simple", "example.Simple2"));
		//
		//		// Now running 'boo()' which did not exist in the original. Remember this is invoked via proxy and so will only work
		//		// if the proxy was autoregenerated and reloaded!
		//		output = runMethodAndCollectOutput(clazz, "run");
		//		assertContains("[void example.Simple.boo()]", output);
		//		assertDoesNotContain("Simple2.boo running()", output);
	}

	// TODO tests:
	// - cglib representative tests?
	// - supertype not reloadable
	// - multiple types of the same name floating about?
	// - reflective invocation across classloaders
	// - crossloader field access

	// TODO catcher methods for interface methods where the class is an abstract class (it may not have a method in, so needs a catcher, in case it is
	// filled in later)

	// TODO optimizations:
	// set the superclass for a reloadabletype when the clinit runs for the subtype, for quicker lookup of exactly the type we need (getRegistryFor(clazz.getClassLoader())
	// use those sparse arrays and ID numbers more so that type lookups can be quicker.  Once we truly discover the right super type reference from a subtype, fill it in in the array
	// optimize for the case where there will only be one type around of a given name *usually*

	@Test
	public void github34() throws Exception {
		ReloadableType rtypeA = subLoader.loadAsReloadableType("issue34.Implementation3");
		result = runUnguarded(rtypeA.getClazz(), "run");
		assertEquals("Hello World!", result.stdout);

		//		ReloadableType rtypeB = subLoader.loadAsReloadableType("subpkg.Bottom");
		//		result = runUnguarded(rtypeB.getClazz(), "m");
		//		assertEquals("Bottom.m() running", result.stdout);
		//		assertNotSame(rtypeA.getTypeRegistry(), rtypeB.getTypeRegistry());
	}


}
